Xposed
网上关于Xposed的介绍很多,但都是点到为止,比如:
在Android系统中,应用程序进程以及系统服务进程SystemServer都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的,Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例,这也是Xposed选择替换app_process的原因。
Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会注册一些Android核心类的JNI方法到Dalvik虚拟机实例中去,以及将Java运行时库加载到进程中来。而一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,这也就是可以将XposedBridge这个jar包加载到每一个Android应用程序中的原因,
我当然不会满足于这么一点浅薄的介绍,既然用这个框架了,那就得把这个框架搞清楚对不?
一句话原理:
Xposed框架的原理是通过替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。
为什么是app_process
Android系统是基于Linux内核的,而在Linux系统中,所有的进程都是init进程的子孙进程,也就是说,所有的进程都是直接或者间接地由init进程fork出来的。Zygote进程也不例外,它是在系统启动的过程,由init进程创建的。在系统启动脚本system/core/rootdir/init.rc文件中,我们可以看到启动Zygote进程的脚本命令:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
系统启动之后就可以在/dev/socket目录下看到有一个名为zygote的文件,就是zygote占用的socket端口。
所以,zygote是由app_process启动的,替换app_process后,启动的就是Xposed之后的zygote了。
为什么XposedBridge可以生效
Xposed版zygote进程在启动时会创建一个Dalvik虚拟机实例,以及注册一些Android核心类的JNI方法到Dalvik虚拟机实例中去。同时Xposed版zygote把XposedBridge.jar添加到CLASSPATH环境变量,并将Java运行时库加载到进程中。一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,所以XposedBridge.jar可以被加载到每一个Android应用程序中。
zygote进程加载XposedBridge将所有需要替换的Method通过JNI方法hookMethodNative指向Native方法
xposedCallHandler,xposedCallHandler在转入handleHookedMethod这个Java方法执行用户规定的Hook Func。
Xposed版zygote在启动时还会获得一个JNIEnv实例,该实例描述的是zygote进程的主线程的JNI环境,Xposed版zygote进程通过JNIEnv实例的成员函数CallStaticVoidMethod()调用de.robv.android.xposed.XposedBridge的main函数作为java代码的入口点。
de.robv.android.xposed.XposedBridge.main函数做了以下几件事:
(1) 初始化xposed框架。
(2) 调用initForZygote()方法hook应用进程创建时调用的一些关键函数,比如通过挂钩LoadedApk的构造函数获得应用进程的相关信息并保存至XC_LoadPackage.LoadPackageParam的实例中,该实例在后续hook应用程序中的函数时可用于获取应用程序相关信息。通过挂钩handleBindApplication方法,可以在应用程序启动时调用所有IXposedHookLoadPackage类型的钩子(其实最终调用的是IXposedHookLoadPackage的handleLoadPackage方法)。该类型的钩子用于对应用程序进行挂钩,假如要hook应用程序中的函数,我们编写的xposed插件中的钩子类必须实现IXposedHookLoadPackag接口,重写它的handleLoadPackage方法并在方法体中调用xposed框架提供的挂钩函数(比如findAndHookMethod)hook想要挂钩的应用程序函数。
(3) 调用loadModules()加载所有的xposed插件,将这些插件中不同钩子类型的钩子分别保存起来。有三种类型的钩子,IXposedHookLoadPackage类型的钩子对应用程序挂钩,IXposedHookZygoteInit类型钩子对Zygote的初始化进行挂钩,IXposedHookInitPackageResources类型钩子对资源进行挂钩。
(4) 最后再调用原始的ZygoteInit.main函数,完成zygote的全部初始化工作。
http://4hou.win/wordpress/?p=7516
https://blog.csdn.net/u014385722/article/details/82013306
使用Java反射实现API Hook
通过对 Android 平台的虚拟机注入与 Java 反射的方式,来改变 Android 虚拟机调用函数的方式(ClassLoader),从而达到 Java 函数重定向的目的,这里我们将此类操作称为 Java API Hook。
先从简单的开始,比如尝试Hook按钮的点击事件。
首先先看一下点击事件:
/**
* Interface definition for a callback to be invoked when a view is clicked.
*/
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
我们对Button绑定点击事件:
mBtnHijack = findViewById(R.id.btn_hijack);
mBtnHijack.setOnClickListener(v -> {
Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();
});
所以下一步是看setOnClickListener方法是怎么保存OnClickListener接口的:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
看到OnClickListener被保存到ListenerInfo的成员变量中:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
static class ListenerInfo {
...
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
protected OnContextClickListener mOnContextClickListener;
...
}
而ListenerInfo是View的一个内部类。
既然知道OnClickListener的保存位置,那么我们要Hook点击事件,就是创建一个自己的点击事件,然后替换掉原来的事件即可。
先创建一个实现自己功能的点击事件
class HookedOnClickListener implements View.OnClickListener {
private View.OnClickListener origin; // 原始的点击事件
HookedOnClickListener(View.OnClickListener origin) {
this.origin = origin;
}
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "hook click", Toast.LENGTH_SHORT).show();
Log.i("WOW", "Before click, do what you want to to.");
if (origin != null) {
origin.onClick(v); // 执行原始的点击逻辑
}
Log.i("WOW", "After click, do what you want to to.");
}
}
然后就是使用反射,用我们的OnClickListener替换原来注册的点击回调:
private void hookOnClickListener(View view) {
try {
// 得到 View 的 ListenerInfo 对象
Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
// 强制访问
getListenerInfo.setAccessible(true);
// 执行getListenerInfo拿到对象
Object listenerInfo = getListenerInfo.invoke(view);
// 得到 原始的 ListenerInfo 类
Class> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");
// 从 ListenerInfo找到onClickListener属性
Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");
mOnClickListener.setAccessible(true);
// 用前面的listenerInfo对象获取原始的listener
View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);
// 用自定义的 OnClickListener 替换原始的 OnClickListener
View.OnClickListener hookedOnClickListener = new HookedOnClickListener(originOnClickListener);
mOnClickListener.set(listenerInfo, hookedOnClickListener);
} catch (Exception e) {
Log.w("hook clickListener failed!", e);
}
}
把这段代码放到按钮设置OnClickListener之后:
mBtnHijack.setOnClickListener(v -> {
Toast.makeText(MainActivity.this, "Click button", Toast.LENGTH_LONG).show();
});
hookOnClickListener(mBtnHijack);
这样就完成了对按钮点击事件的Hook。
但是这只能编码Hook自己的应用,这样做的意义是什么呢?
当应用内接入了众多的 SDK,SDK 内部会使用系统服务 NotificationManager 发送通知,这就导致通知难以管理和控制。现在我们就用 Hook 技术拦截部分通知,限制应用内的通知发送操作。
发送通知是由NotificationManager的notify方法实现,通过查看源码,定位到:
public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
{
INotificationManager service = getService();
...
try {
service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, copy, user.getIdentifier());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static INotificationManager sService;
/** @hide */
static public INotificationManager getService()
{
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService("notification");
sService = INotificationManager.Stub.asInterface(b);
return sService;
}
INotificationManager 是跨进程通信的 Binder 类,sService 是 NMS(NotificationManagerService) 在客户端的代理,发送通知要委托给 sService,由它传递给 NMS。我们发现 sService 是个静态成员变量,而且只会初始化一次。只要把 sService 替换成自定义的不就行了么,确实如此。
private void hookNotificationManager(Context context) {
try {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
// 得到系统的 sService
Method getService = NotificationManager.class.getDeclaredMethod("getService");
getService.setAccessible(true);
final Object sService = getService.invoke(notificationManager);
Class iNotiMngClz = Class.forName("android.app.INotificationManager");
// 动态代理 INotificationManager
Object proxyNotiMng = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{iNotiMngClz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.debug("invoke(). method:{}", method);
if (args != null && args.length > 0) {
for (Object arg : args) {
log.debug("type:{}, arg:{}", arg != null ? arg.getClass() : null, arg);
}
}
// 操作交由 sService 处理,不拦截通知
// return method.invoke(sService, args);
// 拦截通知,什么也不做
return null;
// 或者是根据通知的 Tag 和 ID 进行筛选
}
});
// 替换 sService
Field sServiceField = NotificationManager.class.getDeclaredField("sService");
sServiceField.setAccessible(true);
sServiceField.set(notificationManager, proxyNotiMng);
} catch (Exception e) {
log.warn("Hook NotificationManager failed!", e);
}
}
Hook 的时机还是尽量要早,我们在 attachBaseContext 里面操作。
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
hookNotificationManager(newBase);
}
这样我们就完成了对通知的拦截,可见 Hook 技术真的是非常强大,好多插件化的原理都是建立在 Hook 之上的。
总结一下:
- Hook 的选择点:静态变量和单例,因为一旦创建对象,它们不容易变化,非常容易定位。
- Hook 过程:
- 寻找 Hook 点,原则是静态变量或者单例对象,尽量 Hook public 的对象和方法。
- 选择合适的代理方式,如果是接口可以用动态代理。
- 偷梁换柱——用代理对象替换原始对象。
- Android 的 API 版本比较多,方法和类可能不一样,所以要做好 API 的兼容工作。
Xposed Hook微信运动
首先在AndroidManifest.xml Application下添加xposed模块
Gradle添加依赖
compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'
然后再assets目录添加一个xposed_init
文件供Xposed框架访问,内容为包名:
com.softard.xposedemo.HookTest
然后创建我们的HookTest
public class HookTest implements IXposedHookLoadPackage {
// 实现Hook篡改程序
@SuppressLint("PrivateApi")
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (lpparam.packageName.equals("com.tencent.mm")) { // 搞一搞微信
XposedBridge.log("hoooook wechat");
Class> clazz1 = Class.forName(
"android.hardware.SystemSensorManager$SensorEventQueue", true, lpparam.classLoader);
XposedBridge.hookAllMethods(clazz1, "dispatchSensorEvent", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
int times = XSharedPreferencesUtil.getPref().getInt("step", 500);
XposedBridge.log("~~~~~~~Multi times: " + times);
XposedBridge.log("Wechat2222 Sensor param " + ((float[]) param.args[1])[0]);
((float[]) param.args[1])[0] = ((float[]) param.args[1])[0] * times;
XposedBridge.log("final Sensor param " + ((float[]) param.args[1])[0]);
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
}*/
}
}
To be continued